package org.codefilarete.stalactite.engine.runtime.load;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import javax.annotation.Nullable;

import org.codefilarete.reflection.PropertyAccessPoint;
import org.codefilarete.stalactite.dsl.PolymorphismPolicy.SingleTablePolymorphism;
import org.codefilarete.stalactite.engine.runtime.ConfiguredRelationalPersister;
import org.codefilarete.stalactite.engine.runtime.load.EntityJoinTree.JoinType;
import org.codefilarete.stalactite.engine.runtime.load.EntityTreeInflater.RelationIdentifier;
import org.codefilarete.stalactite.engine.runtime.load.EntityTreeInflater.TreeInflationContext;
import org.codefilarete.stalactite.engine.runtime.load.JoinRowConsumer.ForkJoinRowConsumer;
import org.codefilarete.stalactite.mapping.RowTransformer;
import org.codefilarete.stalactite.query.api.Selectable;
import org.codefilarete.stalactite.sql.ddl.structure.Column;
import org.codefilarete.stalactite.sql.ddl.structure.Key;
import org.codefilarete.stalactite.sql.ddl.structure.Table;
import org.codefilarete.stalactite.sql.result.BeanRelationFixer;
import org.codefilarete.stalactite.sql.result.ColumnedRow;
import org.codefilarete.tool.Reflections;
import org.codefilarete.tool.collection.Iterables;

/**
 * Particular {@link JoinNode} made to handle relation from an entity to a collection of some another polymorphic one. Actually relation doesn't
 * make the complexity of that class: polymorphic entity instantiation is the core focus of it. Here are the hot spots:
 * identifier is given by the subclass which find its id in the row (see {@link SingleTablePolymorphicRelationJoinRowConsumer#giveIdentifier}),
 * and while doing it, it remembers which consumer made it. Then while instantiating entity we invoke it to get right entity type (parent mapping
 * would only give parent entity, which, out of being a wrong approach, can be an abstract type). Then instance is filled up with parent properties
 * by calling merging method (see line 98).
 * Finally, {@link SingleTablePolymorphicRelationJoinRowConsumer} must extend {@link ForkJoinRowConsumer} to give next branch to be consumed by
 * {@link EntityTreeInflater} to avoid "dead" branch to be read : we give it according to the consumer which found the identifier. 
 * 
 * @author Guillaume Mary
 */
public class SingleTablePolymorphicRelationJoinNode<C, T1 extends Table<T1>, T2 extends Table<T2>, JOINCOLTYPE, I, DTYPE> extends RelationJoinNode<C, T1, T2, JOINCOLTYPE, I> {
	
	private final Set<ConfiguredRelationalPersister<? extends C, I>> subPersisters;
	
	private final SingleTablePolymorphism<C, DTYPE> polymorphismPolicy;
	
	private final Column<T2, DTYPE> discriminatorColumn;
	
	public SingleTablePolymorphicRelationJoinNode(JoinNode<?, T1> parent,
												  PropertyAccessPoint<?, ?> propertyAccessor,
												  Key<T1, JOINCOLTYPE> leftJoinColumn,
												  Key<T2, JOINCOLTYPE> rightJoinColumn,
												  JoinType joinType,
												  Set<? extends Selectable<?>> columnsToSelect,
												  @Nullable String tableAlias,
												  EntityInflater<C, I> entityInflater,
												  BeanRelationFixer<Object, C> beanRelationFixer,
												  Column<T2, DTYPE> discriminatorColumn,
												  Set<ConfiguredRelationalPersister<? extends C, I>> subPersisters,
												  SingleTablePolymorphism<C, DTYPE> polymorphismPolicy) {
		super(parent, propertyAccessor, leftJoinColumn, rightJoinColumn, joinType, columnsToSelect, tableAlias, entityInflater, beanRelationFixer, null);
		this.discriminatorColumn = discriminatorColumn;
		this.subPersisters = subPersisters;
		this.polymorphismPolicy = polymorphismPolicy;
	}
	
	@Override
	public SingleTablePolymorphicRelationJoinRowConsumer toConsumer(JoinNode<C, T2> joinNode) {
		return new SingleTablePolymorphicRelationJoinRowConsumer(joinNode);
	}
	
	public class SingleTablePolymorphicRelationJoinRowConsumer implements RelationJoinRowConsumer<C, I> {
		
		private final Map<DTYPE, SubEntityDeterminer<? extends C>> subEntityDeterminerPerDiscriminatorValue;

		private final JoinNode<C, ?> joinNode;
		
		private SingleTablePolymorphicRelationJoinRowConsumer(JoinNode<C, ?> joinNode) {
			this.joinNode = joinNode;
			this.subEntityDeterminerPerDiscriminatorValue = Iterables.map(subPersisters,
					subPersister -> polymorphismPolicy.getDiscriminatorValue(subPersister.getClassToPersist()),
					subPersister -> new SubEntityDeterminer<>(subPersister),
					HashMap::new);
		}

		@Override
		public JoinNode<C, ?> getNode() {
			return joinNode;
		}

		@Override
		public EntityReference<C, I> applyRelatedEntity(EntityReference<?, ?> parentJoinEntity, ColumnedRow row, TreeInflationContext context) {
			RowIdentifier<C> rowIdentifier = giveIdentifier();
			if (rowIdentifier == null) {
				return null;
			} else {
				I rightIdentifier = rowIdentifier.entityIdentifier;
				// we avoid treating twice same relation, overall to avoid adding twice same instance to a collection (one-to-many list cases)
				// in case of multiple collections in ResultSet because it creates similar data (through cross join) which are treated as many as
				// collections cross with each other. This also works for one-to-one relations but produces no bugs. It can also be seen as a performance
				// enhancement even if it hasn't been measured.
				RelationIdentifier eventuallyApplied = new RelationIdentifier(
						parentJoinEntity.getIdentifier(),
						getEntityType(),
						rightIdentifier,
						this);
				// primary key null means no entity => nothing to do
				if (context.isTreatedOrAppend(eventuallyApplied)) {
					C rightEntity = context.giveEntityFromCache(getEntityType(), rightIdentifier, () -> rowIdentifier.rowConsumer.createInstance(row));
					getBeanRelationFixer().apply(parentJoinEntity.getEntity(), rightEntity);
					if (getConsumptionListener() != null) {
						getConsumptionListener().onNodeConsumption(rightEntity, row);
					}
					return new EntityReference<>(rightEntity, rightIdentifier);
				} else {
					return null;
				}
			}
		}
		
		@Nullable
		private <D extends C> RowIdentifier<D> giveIdentifier() {
			ColumnedRow row = EntityTreeInflater.currentContext().getDecoder(joinNode);
			DTYPE discriminatorValue = row.get(discriminatorColumn);
			if (discriminatorValue != null) {
				SubEntityDeterminer<D> discriminatorConsumer = (SubEntityDeterminer<D>) subEntityDeterminerPerDiscriminatorValue.get(discriminatorValue);
				if (discriminatorConsumer != null) {
					I identifier = discriminatorConsumer.giveIdentifier(row);
					if (identifier != null) {
						return new RowIdentifier<>(identifier, discriminatorConsumer);
					}
				}
			}
			return null;
		}
		
		/**
		 * Small class that helps to detect if a persister is concerned by a given row and creates the entity if that's the case.
		 * @param <D> persister entity type
		 */
		private class SubEntityDeterminer<D extends C> {
			
			private final Function<ColumnedRow, I> identifierProvider;
			
			private final RowTransformer<D> entityInflater;
			
			public SubEntityDeterminer(ConfiguredRelationalPersister<D, I> subPersister) {
				this.identifierProvider = row -> subPersister.getMapping().getIdMapping().getIdentifierAssembler().assemble(row);
				this.entityInflater = subPersister.getMapping().getRowTransformer();
			}
			
			@Nullable
			public I giveIdentifier(ColumnedRow row) {
				return identifierProvider.apply(row);
			}
			
			public D createInstance(ColumnedRow row) {
				return entityInflater.transform(row);
			}
		}
		
		private class RowIdentifier<D extends C> {
			
			private final I entityIdentifier;
			private final SubEntityDeterminer<D> rowConsumer;
			
			private RowIdentifier(I entityIdentifier, SubEntityDeterminer<D> rowConsumer) {
				this.entityIdentifier = entityIdentifier;
				this.rowConsumer = rowConsumer;
			}
		}
		
		/**
		 * Implemented for debug. DO NOT RELY ON IT for anything else.
		 */
		@Override
		public String toString() {
			return Reflections.toString(this.getClass());
		}
	}
}
